<?php

namespace pmvc\mvc;

use pmvc\io\IOUtil;
use Exception;
use DateTime;

class Response {
	
	const STATUS_OK					= 200;
	const STATUS_CREATED			= 201;
	const STATUS_ACCEPTED			= 202;
	const STATUS_REDIRECT_PERM		= 301;
	const STATUS_REDIRECT_TEMP		= 302;
	const STATUS_REDIRECT_OTHER		= 303;
	const STATUS_BAD_REQUEST		= 400;
	const STATUS_UNAUTHORIZED		= 401;
	const STATUS_NOT_FOUND			= 404;
	const STATUS_METHOD_NOT_ALLOWED	= 405;
	const STATUS_CONFLICT			= 409;
	const STATUS_GONE				= 410;
	const STATUS_INTERNAL_ERROR		= 500;

	const HEADER_CONTENT_LENGTH = "Content-Length";
	const HEADER_LOCATION		= "Location";

	private $model			= NULL;
	private $committed		= FALSE;
	private $stream			= NULL;
	private $headers		= Array();
	private $cookies		= Array();
	private $statusCode		= self::STATUS_OK;
	private $statusMessage	= "OK";
	private $dispatcher		= NULL;

	private $statusMessages = Array(
		self::STATUS_OK					=> "OK",
		self::STATUS_REDIRECT_PERM		=> "Moved Permanently",
		self::STATUS_REDIRECT_TEMP		=> "Found",
		self::STATUS_REDIRECT_OTHER		=> "See Other",
		self::STATUS_BAD_REQUEST		=> "Bad Request",
		self::STATUS_UNAUTHORIZED		=> "Unauthorized",
		self::STATUS_NOT_FOUND			=> "Not Found",
		self::STATUS_METHOD_NOT_ALLOWED	=> "Method Not Allowed",
		self::STATUS_GONE				=> "Gone",
		self::STATUS_INTERNAL_ERROR		=> "Internal Server Error"
	);

	/**
	 * Creates a Response object for the given {@link Dispatcher}.
	 */
	public function __construct(Dispatcher $dispatcher) {
		$this->dispatcher = $dispatcher;
		$this->reset();
	}

	/**
	 * Returns the {@link Model}.
	 */
	public function getModel() {
		if ($this->model==NULL) {
			$this->model = new Model();
		}
		return $this->model;
	}

	/**
	 * Checks to see if the response has been committed or not.
	 * @return bool true if committed, false otherwise
	 */
	public function isCommitted() {
		return $this->committed;
	}

	/**
	 * Sends a redirect header to the browser.  This method clears
	 * anything in the response buffer and immediately sends the
	 * redirect headers.
	 * @param string $toUrl
	 * @param int $statusCode
	 */
	public function sendRedirect($toUrl, $statusCode=self::STATUS_REDIRECT_TEMP) {
		$this->checkCommitted();
		if ($statusCode<300 || $statusCode>399) {
			throw new Exception("Invalid status code for redirection");
		}
		if ($this->stream!=NULL) {
			$this->stream->close();
			$this->stream = NULL;
		}
		$this->setStatus($statusCode, "Redirect");
		$this->removeHeader(Response::HEADER_CONTENT_LENGTH);
		$this->setHeader(Response::HEADER_LOCATION, $toUrl);
		$this->send();
	}

	/**
	 * Sets a response header by it's name and value. Overwrites
	 * headers of the same name that have already been set
	 * @param string $name
	 * @param string $value
	 */
	public function setHeader($name, $value) {
		$this->checkCommitted();
		$this->headers[strtolower($name)] = Array(Array($name, $value));
	}

	/**
	 * Adds a response header by it's name and value.
	 * @param string $name
	 * @param string $value
	 */
	public function addHeader($name, $value) {
		$this->checkCommitted();
		if (!$this->containsHeader($name)) {
			$this->headers[strtolower($name)] = Array();
		}
		$this->headers[strtolower($name)][] = Array($name, $value);
	}

	/**
	 * Removes a header from being sent to the client.
	 * @param string $name the name
	 */
	public function removeHeader($name) {
		$this->checkCommitted();
		if ($this->containsHeader($name)) {
			unset($this->headers[strtolower($name)]);
		}
	}

	/**
	 * Returns all of the headers that are to be
	 * sent to the client.
	 * @return Array the headers
	 */
	public function getHeaders() {
		$ret = Array();
		foreach ($this->headers as $key=>$value) {
			$ret []= $value;
		}
		return $ret;
	}

	/**
	 * Indicates whether or not the given header has been set.
	 * @param string $name the name
	 */
	public function containsHeader($name) {
		return array_key_exists(strtolower($name), $this->headers);
	}

	/**
	 * Sets the status code and message that will be sent
	 * to the client.
	 * @param int $code
	 * @param string $message
	 */
	public function setStatus($code, $message=NULL) {
		$this->checkCommitted();
		if ($message==NULL && array_key_exists($code, $this->statusMessages)) {
			$message = $this->statusMessages[$code];
		}
		$this->statusCode		= $code;
		$this->statusMessage	= $message;
	}

	/**
	 * Resets the response clearing all headers,
	 * status codes, messages and clearing the
	 * output buffer.
	 */
	public function reset() {
		$this->checkCommitted();
		$this->committed		= FALSE;
		$this->headers			= Array();
		$this->setStatus		= self::STATUS_OK;
		$this->statusMessage	= $this->statusMessages[self::STATUS_OK];
		if ($this->stream!=null) {
			$this->stream->close();
			$this->stream = NULL;
		}
	}

	/**
	 * Returns a stream to the output buffer.
	 * @param int $bufferSize the buffer size
	 * @return Stream the stream object
	 */
	public function getStream($bufferSize=-1) {
		$this->checkCommitted();
		if ($this->stream==NULL) {
			$this->stream = IOUtil::openMemory("r+b", $bufferSize);
		}
		return $this->stream;
	}

	/**
	 * Sets a cookie.
	 * @param $name the name
	 * @param $value the value
	 * @param $expire the expiration date (DateTime object, or unix timestamp)
	 * @param $path the path
	 * @param $domain the domain
	 * @param $secure secure
	 * @param $httponly http only
	 */
	public function setCookie($name, $value="", $expire=0, $path="", $domain="", $secure=false, $httponly=false) {

		// parse date
		if (!is_a($expire, "DateTime") && $expire!=0) {
			if (is_numeric($expire)) {
				$ret = new DateTime();
				$expire = $ret->setTimestamp($expire);
			} else {
				$expire = new DateTime($expire);
			}
		}

		// add value
		$val = urlencode($name)."=".urlencode($value);

		// expiration
		if (is_a($expire, "DateTime")) {
			$val .= "; Expires=".$expire->format("D, d-M-Y H:i:s T");
		}

		// path
		if ($path!="") {
			$val .= "; Path=".urlencode($path);
		}

		// domain
		if ($path!="") {
			$val .= "; Domain=".$domain;
		}

		// secure
		if ($secure) {
			$val .= "; Secure";
		}

		// secure
		if ($httponly) {
			$val .= "; HttpOnly";
		}

		// add the header
		$this->cookies[$name] = $val;
	}

	/**
	 * Commits and sends this response to the client.
	 */
	public function send() {
		$this->checkCommitted();

		// send headers
		header("HTTP/1.0 ".$this->statusCode." ".$this->statusMessage);
		foreach ($this->headers as $name => $headers) {
			foreach ($headers as $header) {
				header($header[0].": ".$header[1], false);
			}
		}

		// set cookies
		foreach ($this->cookies as $name => $value) {
			header("Set-Cookie: ".$value, false);
		}

		// make sure we have a content-length
		if (!$this->containsHeader(Response::HEADER_CONTENT_LENGTH)
			&& $this->stream!=NULL
			&& $this->stream->isOpen()) {
			$this->stream->end();
			header("Content-Length: ".$this->stream->position(), true);
		}

		// send data
		if ($this->stream!=NULL && $this->stream->isOpen()) {
			$this->stream->begin();
			$this->stream->passThru();
		}

		// we're commited now
		$this->committed = TRUE;
		if ($this->stream!=NULL) {
			$this->stream->close();
			$this->stream = NULL;
		}
	}

	/**
	 * Internal method for raising exceptions for methods
	 * that don't work after a response has already been committed.
	 */
	private function checkCommitted() {
		if ($this->committed) {
			throw new Exception("Response is already commited");
		}
	}

}

?>